package sale.wxb.loveshopping.service.impl;

import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import sale.wxb.loveshopping.constant.RedisConst;
import sale.wxb.loveshopping.entity.common.ResultException;
import sale.wxb.loveshopping.entity.bo.UserTokenRedisBo;
import sale.wxb.loveshopping.entity.model.SysUser;
import sale.wxb.loveshopping.entity.vo.LoginByAccountVo;
import sale.wxb.loveshopping.entity.vo.RegisterVo;
import sale.wxb.loveshopping.entity.yml.YmlConfigAuth;
import sale.wxb.loveshopping.entity.yml.YmlConfigAuthLogin;
import sale.wxb.loveshopping.enums.LoginModeEnum;
import sale.wxb.loveshopping.enums.ResponseEnum;
import sale.wxb.loveshopping.security.UserDetailEntity;
import sale.wxb.loveshopping.service.IAuthenticationService;
import sale.wxb.loveshopping.service.ISysUserService;
import sale.wxb.loveshopping.service.IVerifyCodeService;
import sale.wxb.loveshopping.utils.RedisUtil;

import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import java.util.Date;

@Transactional
@Service
public class AuthenticationServiceImpl implements IAuthenticationService {
    @Autowired
    private YmlConfigAuth ymlConfigAuth;
    @Autowired
    private YmlConfigAuthLogin ymlConfigAuthLogin;
    @Autowired
    private HttpServletRequest httpServletRequest;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private IVerifyCodeService verifyCodeService;

    /**
     * 写入用户登录成功信息
     * @param sysUser 用户信息
     * @return token
     */
    private String writeLoginUserInfo(SysUser sysUser, LoginModeEnum loginModeEnum) {
        // 先清除之前的
        removeRedisCache(sysUser);
        String remoteIp = httpServletRequest.getRemoteAddr();
        String token = DigestUtil.sha256Hex(IdUtil.simpleUUID());
        UserTokenRedisBo userToken = new UserTokenRedisBo();
        userToken.setSysUser(sysUser);
        userToken.setLoginTime(new Date());
        userToken.setRemoteIp(remoteIp);
        // 创建缓存
        redisUtil.set(redisUtil.getKey(RedisConst.TOKEN_USER, token), JSONUtil.toJsonStr(userToken), ymlConfigAuthLogin.getOverdue());
        // 使用用户ID写入token 以便用于其他地方登录时删除旧的token实现不允许多点同时登录使用的功能
        if (!ymlConfigAuthLogin.isMultipoint()) {
            // 保持7天
            redisUtil.set(redisUtil.getKey(RedisConst.USER_TOKEN, sysUser.getId().toString()), token, 604800);
        }
        // 更新用户本次登录IP
        sysUser.setLastLoginIp(remoteIp);
        sysUserService.updateById(sysUser);
        return token;
    }

    /**
     * 清除登录缓存
     * @param sysUser 用户信息
     */
    private void removeRedisCache(SysUser sysUser) {
        String token = (String) redisUtil.get(redisUtil.getKey(RedisConst.USER_TOKEN, sysUser.getId().toString()));

        if (!StringUtils.hasText(token)) {
            return;
        }

        redisUtil.del(redisUtil.getKey(RedisConst.TOKEN_USER, token));
        redisUtil.del(redisUtil.getKey(RedisConst.USER_TOKEN, sysUser.getId().toString()));
    }

    /**
     * 检查是否需要锁定账户 调用一次增加一次失败次数 次数达到后锁定用户
     */
    private void setLocked(String account, boolean reset) {
        String key = redisUtil.getKey(RedisConst.ACCOUNT_LOCK, account);

        if (reset) {
            redisUtil.del(key);
            return;
        }

        if (redisUtil.hasKey(key)) {
            redisUtil.incr(key, 1);
        } else {
            redisUtil.set(key, 1, ymlConfigAuthLogin.getLockDuration());
        }
    }

    @Override
    public String loginByAccount(@NotNull LoginByAccountVo accountVo) {
        return loginByAccount(accountVo, true);
    }

    public String loginByAccount(@NotNull LoginByAccountVo accountVo, boolean isValidation) {
        if (isValidation) {
            // 校验验证码 校验失败内部会抛出异常
            verifyCodeService.verifyImageCode(accountVo.getVerifyCode());
        }
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountVo.getAccount(), accountVo.getPassword());
        Authentication authenticate;

        try {
            authenticate = authenticationManager.authenticate(authenticationToken);
        } catch (BadCredentialsException ex) {
            setLocked(accountVo.getAccount(), false);
            throw new ResultException(ResponseEnum.UP_FAIL);
        }

        UserDetailEntity userDetail = (UserDetailEntity) authenticate.getPrincipal();
        String token = writeLoginUserInfo(userDetail.getUser(), LoginModeEnum.ACCOUNT_PASSWORD);
        setLocked(accountVo.getAccount(), true);
        return token;
    }

    @Override
    public Boolean logout() {
        String token = httpServletRequest.getHeader(ymlConfigAuth.getHeaderTokenKey());

        if (StringUtils.hasText(token)) {
            redisUtil.del(redisUtil.getKey(RedisConst.TOKEN_USER, token));
        }

        return true;
    }

    @Override
    public Long register(RegisterVo body, String verifyCode) {
        // 检查验证码是否正确 验证失败内部会抛出异常
        verifyCodeService.verifyImageCode(verifyCode);
        return sysUserService.createAccount(body);
    }

    @Override
    public String registerAndLogin(RegisterVo body, String verifyCode) {
        Long userId = register(body, verifyCode);

        if (userId == null) {
            throw new ResultException("注册账号失败");
        }

        LoginByAccountVo login = new LoginByAccountVo();
        login.setAccount(body.getAccount());
        login.setPassword(body.getPassword());
        return loginByAccount(login, false);
    }
}
